/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

import type {BaseSelection, LexicalCommand, LexicalEditor, NodeKey,} from 'lexical';
import {
    $getNodeByKey,
    $getSelection,
    $isNodeSelection,
    $isRangeSelection,
    $setSelection,
    CLICK_COMMAND,
    COMMAND_PRIORITY_LOW,
    createCommand,
    DRAGSTART_COMMAND,
    KEY_ENTER_COMMAND,
    KEY_ESCAPE_COMMAND,
    SELECTION_CHANGE_COMMAND,
} from 'lexical';
import {JSX, Suspense, useCallback, useEffect, useRef, useState} from 'react';

import './ImageNode.css';

import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {useCollaborationContext} from '@lexical/react/LexicalCollaborationContext';
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
import {mergeRegister} from '@lexical/utils';

import {createWebsocketProvider} from '../collaboration';
import {useEditorSettings} from '../context/EditorSettingsContext.tsx';
import {useSharedHistoryContext} from '../context/SharedHistoryContext';
import brokenImage from '../images/image-broken.svg';
import EmojisPlugin from '../plugins/EmojisPlugin';
import KeywordsPlugin from '../plugins/KeywordsPlugin';
import LinkPlugin from '../plugins/LinkPlugin';
import MentionsPlugin from '../plugins/MentionsPlugin';
import TreeViewPlugin from '../plugins/TreeViewPlugin';
import ContentEditable from '../ui/ContentEditable';
import ImageResizer from '../ui/ImageResizer';
import {$isImageNode} from './ImageNode';
import {convertFileSrc} from "@tauri-apps/api/core";
import {useFile} from "../context/FileContext.tsx";
import {useSettings} from "../../settings/SettingsContext.tsx";

const imageCache = new Map<string, Promise<boolean> | boolean>();

export const RIGHT_CLICK_IMAGE_COMMAND: LexicalCommand<MouseEvent> =
    createCommand('RIGHT_CLICK_IMAGE_COMMAND');

function useSuspenseImage(src: string) {
    let cached = imageCache.get(src);
    if (typeof cached === 'boolean') {
        return cached;
    } else if (!cached) {
        cached = new Promise<boolean>(async (resolve) => {
            const img = new Image();
            img.src = src;
            img.onload = () => resolve(false);
            img.onerror = () => resolve(true);
        }).then((hasError) => {
            imageCache.set(src, hasError);
            return hasError;
        });
        imageCache.set(src, cached);
        throw cached;
    }
    throw cached;
}

function isSVG(src: string): boolean {
    return src.toLowerCase().endsWith('.svg');
}

function LazyImage({
                       altText,
                       className,
                       imageRef,
                       src,
                       width,
                       height,
                       maxWidth,
                       onError,
                   }: {
    altText: string;
    className: string | null;
    height: 'inherit' | number;
    imageRef: { current: null | HTMLImageElement };
    maxWidth: number;
    src: string;
    width: 'inherit' | number;
    onError: () => void;
}): JSX.Element {
    const settings = useSettings();
    const [dimensions, setDimensions] = useState<{
        width: number;
        height: number;
    } | null>(null);
    const file = useFile();
    let newSrc: string;

    if (src.startsWith("/")) {
        newSrc = convertFileSrc(src);
    } else if (src.startsWith("http") || src.startsWith("data:")) {
        newSrc = src;
    } else {
        if (settings.image?.action == 1) {
            if (settings.image?.preferRelativeFolder) {
                newSrc = convertFileSrc(decodeURI(file.path?.split(/\/|\\\\/).slice(0, -1).join("/") + "/" + src))
            } else {
                if (settings.image?.globalDir) {
                    newSrc = convertFileSrc(decodeURI(settings.image?.globalDir + "/" + src))
                } else {
                    newSrc = convertFileSrc(decodeURI(src));
                }
            }
        } else {
            newSrc = convertFileSrc(decodeURI(src));
        }

    }
    const isSVGImage = isSVG(newSrc);

    // Set initial dimensions for SVG images
    useEffect(() => {
        if (imageRef.current && isSVGImage) {
            const {naturalWidth, naturalHeight} = imageRef.current;
            setDimensions({
                height: naturalHeight,
                width: naturalWidth,
            });
        }
    }, [imageRef, isSVGImage]);

    //process the mixed usage of image path
    let hasError = useSuspenseImage(newSrc);
    if (hasError) {
        if (settings.image?.preferRelativeFolder && settings.image?.globalDir) {
            newSrc = convertFileSrc(decodeURI(settings.image?.globalDir + "/" + src))
            hasError = useSuspenseImage(newSrc);
        } else {
            newSrc = convertFileSrc(decodeURI(file.path?.split(/\/|\\\\/).slice(0, -1).join("/") + "/" + src))
            hasError = useSuspenseImage(newSrc);
        }
    }

    useEffect(() => {
        if (hasError) {
            onError();
        }
    }, [hasError, onError]);

    if (hasError) {
        return <BrokenImage/>;
    }

    // Calculate final dimensions with proper scaling
    const calculateDimensions = () => {
        if (!isSVGImage) {
            //TODO shunyun 2025/5/2: size can be set by user
            return {
                height,
                maxWidth: '100%',
                width,
            };
        }

        // Use natural dimensions if available, otherwise fallback to defaults
        const naturalWidth = dimensions?.width || 200;
        const naturalHeight = dimensions?.height || 200;

        let finalWidth = naturalWidth;
        let finalHeight = naturalHeight;

        // Scale down if width exceeds maxWidth while maintaining aspect ratio
        if (finalWidth > maxWidth) {
            const scale = maxWidth / finalWidth;
            finalWidth = maxWidth;
            finalHeight = Math.round(finalHeight * scale);
        }

        // Scale down if height exceeds maxHeight while maintaining aspect ratio
        const maxHeight = 500;
        if (finalHeight > maxHeight) {
            const scale = maxHeight / finalHeight;
            finalHeight = maxHeight;
            finalWidth = Math.round(finalWidth * scale);
        }

        return {
            height: finalHeight,
            maxWidth,
            width: finalWidth,
        };
    };

    const imageStyle = calculateDimensions();

    return (
        <img
            className={className || undefined}
            src={newSrc}
            alt={altText}
            ref={imageRef}
            style={imageStyle}
            onError={onError}
            draggable="false"
            onLoad={(e) => {
                if (isSVGImage) {
                    const img = e.currentTarget;
                    setDimensions({
                        height: img.naturalHeight,
                        width: img.naturalWidth,
                    });
                }
            }}
        />
    );
}

function BrokenImage(): JSX.Element {
    return (
        <img
            src={brokenImage}
            style={{
                height: 200,
                opacity: 0.2,
                width: 200,
            }}
            draggable="false"
            alt="Broken image"
        />
    );
}

export default function ImageComponent({
                                           src,
                                           altText,
                                           nodeKey,
                                           width,
                                           height,
                                           maxWidth,
                                           resizable,
                                           showCaption,
                                           caption,
                                           captionsEnabled,
                                       }: {
    altText: string;
    caption: LexicalEditor;
    height: 'inherit' | number;
    maxWidth: number;
    nodeKey: NodeKey;
    resizable: boolean;
    showCaption: boolean;
    src: string;
    width: 'inherit' | number;
    captionsEnabled: boolean;
}): JSX.Element {
    const imageRef = useRef<null | HTMLImageElement>(null);
    const buttonRef = useRef<HTMLButtonElement | null>(null);
    const [isSelected, setSelected, clearSelection] =
        useLexicalNodeSelection(nodeKey);
    const [isResizing, setIsResizing] = useState<boolean>(false);
    const {isCollabActive} = useCollaborationContext();
    const [editor] = useLexicalComposerContext();
    const [selection, setSelection] = useState<BaseSelection | null>(null);
    const activeEditorRef = useRef<LexicalEditor | null>(null);
    const [isLoadError, setIsLoadError] = useState<boolean>(false);
    const isEditable = useLexicalEditable();
    //@ts-ignore
    const [imageSrc, setImageSrc] = useState<string>(src);

    const $onEnter = useCallback(
        (event: KeyboardEvent) => {
            const latestSelection = $getSelection();
            const buttonElem = buttonRef.current;
            if (
                isSelected &&
                $isNodeSelection(latestSelection) &&
                latestSelection.getNodes().length === 1
            ) {
                if (showCaption) {
                    // Move focus into nested editor
                    $setSelection(null);
                    event.preventDefault();
                    caption.focus();
                    return true;
                } else if (
                    buttonElem !== null &&
                    buttonElem !== document.activeElement
                ) {
                    event.preventDefault();
                    buttonElem.focus();
                    return true;
                }
            }
            return false;
        },
        [caption, isSelected, showCaption],
    );

    const $onEscape = useCallback(
        (event: KeyboardEvent) => {
            if (
                activeEditorRef.current === caption ||
                buttonRef.current === event.target
            ) {
                $setSelection(null);
                editor.update(() => {
                    setSelected(true);
                    const parentRootElement = editor.getRootElement();
                    if (parentRootElement !== null) {
                        parentRootElement.focus();
                    }
                });
                return true;
            }
            return false;
        },
        [caption, editor, setSelected],
    );

    const onClick = useCallback(
        (payload: MouseEvent) => {
            const event = payload;

            if (isResizing) {
                return true;
            }
            if (event.target === imageRef.current) {
                if (event.shiftKey) {
                    setSelected(!isSelected);
                } else {
                    clearSelection();
                    setSelected(true);
                }
                return true;
            }

            return false;
        },
        [isResizing, isSelected, setSelected, clearSelection],
    );

    const onRightClick = useCallback(
        (event: MouseEvent): void => {
            editor.getEditorState().read(() => {
                const latestSelection = $getSelection();
                const domElement = event.target as HTMLElement;
                if (
                    domElement.tagName === 'IMG' &&
                    $isRangeSelection(latestSelection) &&
                    latestSelection.getNodes().length === 1
                ) {
                    editor.dispatchCommand(
                        RIGHT_CLICK_IMAGE_COMMAND,
                        event as MouseEvent,
                    );
                }
            });
        },
        [editor],
    );

    useEffect(() => {
        const rootElement = editor.getRootElement();
        const unregister = mergeRegister(
            editor.registerUpdateListener(({editorState}) => {
                const updatedSelection = editorState.read(() => $getSelection());
                if ($isNodeSelection(updatedSelection)) {
                    setSelection(updatedSelection);
                } else {
                    setSelection(null);
                }
            }),
            editor.registerCommand(
                SELECTION_CHANGE_COMMAND,
                (_, activeEditor) => {
                    activeEditorRef.current = activeEditor;
                    return false;
                },
                COMMAND_PRIORITY_LOW,
            ),
            editor.registerCommand<MouseEvent>(
                CLICK_COMMAND,
                onClick,
                COMMAND_PRIORITY_LOW,
            ),
            editor.registerCommand<MouseEvent>(
                RIGHT_CLICK_IMAGE_COMMAND,
                onClick,
                COMMAND_PRIORITY_LOW,
            ),
            editor.registerCommand(
                DRAGSTART_COMMAND,
                (event) => {
                    if (event.target === imageRef.current) {
                        // TODO This is just a temporary workaround for FF to behave like other browsers.
                        // Ideally, this handles drag & drop too (and all browsers).
                        event.preventDefault();
                        return true;
                    }
                    return false;
                },
                COMMAND_PRIORITY_LOW,
            ),
            editor.registerCommand(KEY_ENTER_COMMAND, $onEnter, COMMAND_PRIORITY_LOW),
            editor.registerCommand(
                KEY_ESCAPE_COMMAND,
                $onEscape,
                COMMAND_PRIORITY_LOW,
            ),
        );

        rootElement?.addEventListener('contextmenu', onRightClick);

        return () => {
            unregister();
            rootElement?.removeEventListener('contextmenu', onRightClick);
        };
    }, [
        clearSelection,
        editor,
        isResizing,
        isSelected,
        nodeKey,
        $onEnter,
        $onEscape,
        onClick,
        onRightClick,
        setSelected,
    ]);

    const setShowCaption = () => {
        editor.update(() => {
            const node = $getNodeByKey(nodeKey);
            if ($isImageNode(node)) {
                node.setShowCaption(true);
            }
        });
    };

    const onResizeEnd = (
        nextWidth: 'inherit' | number,
        nextHeight: 'inherit' | number,
    ) => {
        // Delay hiding the resize bars for click case
        setTimeout(() => {
            setIsResizing(false);
        }, 200);

        editor.update(() => {
            const node = $getNodeByKey(nodeKey);
            if ($isImageNode(node)) {
                node.setWidthAndHeight(nextWidth, nextHeight);
            }
        });
    };

    const onResizeStart = () => {
        setIsResizing(true);
    };

    const {historyState} = useSharedHistoryContext();
    const {
        settings: {showNestedEditorTreeView},
    } = useEditorSettings();

    const draggable = isSelected && $isNodeSelection(selection) && !isResizing;
    const isFocused = (isSelected || isResizing) && isEditable;
    return (
        <Suspense fallback={null}>
            <>
                <div draggable={draggable}>
                    {isLoadError ? (
                        <BrokenImage/>
                    ) : (
                        <LazyImage
                            className={
                                isFocused
                                    ? `focused ${$isNodeSelection(selection) ? 'draggable' : ''}`
                                    : null
                            }
                            src={imageSrc}
                            altText={altText}
                            imageRef={imageRef}
                            width={width}
                            height={height}
                            maxWidth={maxWidth}
                            onError={() => {
                                setIsLoadError(true)
                            }}
                        />
                    )}
                </div>

                {showCaption && (
                    <div className="image-caption-container">
                        <LexicalNestedComposer initialEditor={caption}>
                            <AutoFocusPlugin/>
                            <MentionsPlugin/>
                            <LinkPlugin/>
                            <EmojisPlugin/>
                            <HashtagPlugin/>
                            <KeywordsPlugin/>
                            {isCollabActive ? (
                                <CollaborationPlugin
                                    id={caption.getKey()}
                                    providerFactory={createWebsocketProvider}
                                    shouldBootstrap={true}
                                />
                            ) : (
                                <HistoryPlugin externalHistoryState={historyState}/>
                            )}
                            <RichTextPlugin
                                contentEditable={
                                    <ContentEditable
                                        placeholder="Enter a caption..."
                                        placeholderClassName="ImageNode__placeholder"
                                        className="ImageNode__contentEditable"
                                    />
                                }
                                ErrorBoundary={LexicalErrorBoundary}
                            />
                            {showNestedEditorTreeView === true ? <TreeViewPlugin/> : null}
                        </LexicalNestedComposer>
                    </div>
                )}
                {resizable && $isNodeSelection(selection) && isFocused && (
                    <ImageResizer
                        showCaption={showCaption}
                        setShowCaption={setShowCaption}
                        editor={editor}
                        buttonRef={buttonRef}
                        imageRef={imageRef}
                        maxWidth={maxWidth}
                        onResizeStart={onResizeStart}
                        onResizeEnd={onResizeEnd}
                        captionsEnabled={!isLoadError && captionsEnabled}
                    />
                )}
            </>
        </Suspense>
    );
}
